<?php namespace Reezy\APISign;

use Psr\Http\Message\ServerRequestInterface;
use function array_intersect;
use function array_map;
use function array_values;
use function hash_hmac;
use function hash_hmac_algos;
use function in_array;
use function join;
use function ksort;
use function preg_split;
use function strpos;
use function strtolower;
use function strtotime;
use function strtoupper;
use function trim;

class APISign
{

    private $accessKey;

    private $secretKey;

    private $algo;

    private $ttl;

    private $allow_headers;

    private $xDateKey;
    private $xSignKey;
    private $xAlgoKey;
    private $xHeadersKey;

    /**
     * APISign constructor.
     * @param string $accessKey
     * @param array $config
     * @param array $headerKeys
     */
    public function __construct(string $accessKey, array $config, array $headerKeys)
    {
        $this->accessKey = $accessKey;

        $this->secretKey = $config['secret_key'];
        $this->algo = in_array($config['algo'], hash_hmac_algos()) ? $config['algo'] : 'sha256';
        $this->ttl = $config['ttl'] ?? 0;
        $this->allow_headers = array_map('strtolower', $config['allow_headers'] ?? []);

        $this->xSignKey = $headerKeys['sign_key'] ?? 'x-sign';
        $this->xDateKey = $headerKeys['date_key'] ?? 'date';
        $this->xAlgoKey = $headerKeys['algo_key'] ?? 'x-sign-algo';
        $this->xHeadersKey = $headerKeys['headers_key'] ?? 'x-sign-headers';
    }

    public function check(ServerRequestInterface $request)
    {

        $sign = $request->getHeaderLine($this->xSignKey) ?: $request->getQueryParams()[$this->xSignKey] ?? '';

        $method = strtoupper($request->getMethod());
        $path = '/' . trim($request->getUri()->getPath(), '/');
        $date = $request->getHeaderLine($this->xDateKey);
        $query = $this->getSortedQuery($request);
        $head = $this->getSignedHeaders($request);
        $body = $this->getBodyHash($request);
        $algo = $this->getSignAlgo($request);

        if ($sign !== $this->sign($algo, $method, $path, $query, $date, $head, $body)) {
            throw new APISignException('verify failed');
        }

        if (!$this->checkTtl($date)) {
            throw new APISignException('sign dead');
        }
    }

    private function checkTtl($date)
    {
        if ($this->ttl < 1) return true;
        if (empty($date)) return false;

        $time = strtotime($date);
        if ($time === false || time() - $time > $this->ttl) {
            return false;
        }
        return true;
    }

    private function sign($algo, $method, $path, $query, $date, $head, $body)
    {
        return hash_hmac($algo, join('|', [$this->accessKey, $method, $path, $query, $date, $head, $body]), $this->secretKey);
    }

    private function getSignAlgo(ServerRequestInterface $request) {
        if ($request->hasHeader($this->xAlgoKey)) {
            $algo = $request->getHeaderLine($this->xAlgoKey);
            if (in_array($algo, hash_hmac_algos())) {
                return $algo;
            }
        }
        return $this->algo;
    }

    private function getSortedQuery(ServerRequestInterface $request)
    {
        $params = $request->getQueryParams();

        unset($params[$this->xSignKey]);
        ksort($params);

        $result = '';
        foreach ($params as $k => $v) {
            if (empty($result)) {
                $result = $k . '=' . $v;
            } else {
                $result .= '&' . $k . '=' . $v;
            }
        }
        return $result;
    }

    private function getSignedHeaders(ServerRequestInterface $request)
    {
        if ($request->hasHeader($this->xHeadersKey)) {
            return '';
        }
        $headers = preg_split("/\s*,\s*/", trim($request->getHeaderLine($this->xHeadersKey)));
        if (empty($headers)) {
            return '';
        }
        if (!empty($this->allow_headers)) {
            $headers = array_values(array_intersect($headers, $this->allow_headers));
        }

        $result = '';
        foreach ($headers as $key) {
            if ($header = $request->getHeaderLine($key)) {
                if (empty($result)) {
                    $result = "$key=$header";
                } else {
                    $result .= "&$key=$header";
                }
            }
        }
        return $result;
    }

    private function getBodyHash(ServerRequestInterface $request)
    {
        $body = $request->getBody()->getContents();
        if (empty($body)) {
            return '';
        }
        $contentType = $request->getHeaderLine('content-type');
//        Log::debug("hashBody ==>>> [$body, $contentType]");
        if (strpos($contentType, 'application/x-www-form-urlencoded') !== false || strpos($contentType, 'application/json') !== false) {
            return strtolower(hash('md5', $body));
        }
        return '';
    }

}
